﻿using System.Collections.Generic;
using Deferred.Library.Scene;
using Deferred.Library.Scene.Visual;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;

namespace Deferred.Library.Deferred
{
    class SpotLightRenderEngine : GraphicalComponent
    {
        private DualContentManager content;
        private Effect drawLightDebugEffect;
        private Effect spotLightEffect;
        private Model spotLightCone;

        private EffectParameter cameraPositionParameter;
        private EffectParameter inverseViewProjParameter;
        private EffectParameter lightColorParameter;
        private EffectParameter lightPositionParameter;
        private EffectParameter worldParameter;
        private EffectParameter lightRadiusParameter;
        private EffectParameter angleCosineParameter;
        private EffectParameter decayExponentParameter;
        private EffectParameter spotDirectionParameter;
        private EffectParameter lightViewProjParameter;
        private EffectParameter shadowParameter;
        private EffectParameter debugWorldParameter;
        private EffectParameter debugLightColorParameter;
        private EffectParameter viewProjParameter;

        private RenderTarget2D shadowRT;
        private DepthStencilBuffer shadowBuffer;
        private Texture2D shadowTexture;
        private Matrix lightViewProj;

        private ForwardSceneRenderEngine forwardRenderer;

        public SpotLightRenderEngine(DeferredBase deferredBase)
            : base(deferredBase.DeviceService)
        {
            this.content = deferredBase.Content;
            forwardRenderer = new ForwardSceneRenderEngine(deferredBase);
        }

        public void Initialize()
        {
            drawLightDebugEffect = content.InternalContent.Load<Effect>("DrawLightDebug");
            spotLightEffect = content.InternalContent.Load<Effect>("SpotLight");
            spotLightCone = content.InternalContent.Load<Model>("cone");

            cameraPositionParameter = spotLightEffect.Parameters["cameraPosition"];
            inverseViewProjParameter = spotLightEffect.Parameters["InverseViewProj"];
            lightColorParameter = spotLightEffect.Parameters["lightColor"];
            lightPositionParameter = spotLightEffect.Parameters["lightPosition"];
            worldParameter = spotLightEffect.Parameters["World"];
            lightRadiusParameter = spotLightEffect.Parameters["lightRadius"];
            angleCosineParameter = spotLightEffect.Parameters["angleCosine"];
            decayExponentParameter = spotLightEffect.Parameters["decayExponent"];
            spotDirectionParameter = spotLightEffect.Parameters["spotDirection"];
            viewProjParameter = spotLightEffect.Parameters["ViewProj"];
            lightViewProjParameter = spotLightEffect.Parameters["lightViewProj"];
            shadowParameter = spotLightEffect.Parameters["shadow"];
            debugWorldParameter = drawLightDebugEffect.Parameters["World"];
            debugLightColorParameter = drawLightDebugEffect.Parameters["lightColor"];

            forwardRenderer.Initialize();
        }

        protected override void LoadContent()
        {
            shadowRT = new RenderTarget2D(GraphicsDevice, 2048, 2048, 1, SurfaceFormat.Single);
            shadowBuffer = new DepthStencilBuffer(GraphicsDevice, 2048, 2048, DepthFormat.Depth24);
        }

        public void drawSpotLights(IList<SpotLight> lights, CameraVisual camera, SceneGraphVisual scene)
        {
            cameraPositionParameter.SetValue(camera.Position);
            inverseViewProjParameter.SetValue(Matrix.Invert(camera.View * camera.Projection));

            foreach (SpotLight light in lights)
            {
                DrawSpotLight(light, camera, scene);
            }
        }

        private void DrawSpotLight(SpotLight light, CameraVisual camera, SceneGraphVisual scene)
        {
            DrawShadowMap(light, scene);

            lightColorParameter.SetValue(light.Color.ToVector3());
            lightPositionParameter.SetValue(light.Position);
            worldParameter.SetValue(light.World);
            lightRadiusParameter.SetValue(light.Radius);
            angleCosineParameter.SetValue(light.AngleCosine);
            decayExponentParameter.SetValue(light.DecayExponent);
            spotDirectionParameter.SetValue(light.NormalLookAt);
            lightViewProjParameter.SetValue(lightViewProj);
            shadowParameter.SetValue(shadowTexture);

            if (isCameraInCone(light, camera))
            {
                GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;
            }
            else
            {
                GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;
            }
            GraphicsDevice.RenderState.DepthBufferEnable = false;
            GraphicsDevice.RenderState.AlphaBlendEnable = true;

            spotLightEffect.Begin();
            foreach (EffectPass pass in spotLightEffect.Techniques["SpotLight"].Passes)
            {
                pass.Begin();
                foreach (ModelMesh mesh in spotLightCone.Meshes)
                {
                    foreach (ModelMeshPart part in mesh.MeshParts)
                    {
                        GraphicsDevice.VertexDeclaration = part.VertexDeclaration;
                        GraphicsDevice.Vertices[0].SetSource(mesh.VertexBuffer, part.StreamOffset, part.VertexStride);
                        GraphicsDevice.Indices = mesh.IndexBuffer;
                        GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, part.BaseVertex, 0, part.NumVertices, part.StartIndex, part.PrimitiveCount);
                    }
                }
                pass.End();
            }
            spotLightEffect.End();

            GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace;
        }

        public void DrawSpotLightsDebug(IList<SpotLight> lights)
        {
            foreach (SpotLight light in lights)
            {
                DrawSpotLightDebug(light);
            }
        }

        private void DrawSpotLightDebug(SpotLight light)
        {
            debugWorldParameter.SetValue(light.World);
            debugLightColorParameter.SetValue(light.Color.ToVector4());
            foreach (EffectPass pass in drawLightDebugEffect.Techniques["DrawLightDebug"].Passes)
            {
                pass.Begin();
                foreach (ModelMesh mesh in spotLightCone.Meshes)
                {
                    foreach (ModelMeshPart part in mesh.MeshParts)
                    {
                        GraphicsDevice.VertexDeclaration = part.VertexDeclaration;
                        GraphicsDevice.Vertices[0].SetSource(mesh.VertexBuffer, part.StreamOffset, part.VertexStride);
                        GraphicsDevice.Indices = mesh.IndexBuffer;
                        GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, part.BaseVertex, 0, part.NumVertices, part.StartIndex, part.PrimitiveCount);
                    }
                }
                pass.End();
            }
        }

        private void DrawShadowMap(SpotLight light, SceneGraphVisual scene)
        {
            lightViewProj = calculateLightViewProjection(light);

            Matrix oldViewProj = viewProjParameter.GetValueMatrix();
            viewProjParameter.SetValue(lightViewProj);
            Matrix test = viewProjParameter.GetValueMatrix();

            DepthStencilBuffer oldBuffer = GraphicsDevice.DepthStencilBuffer;
            RenderTarget2D oldTarget = (RenderTarget2D)GraphicsDevice.GetRenderTarget(0);
            GraphicsDevice.DepthStencilBuffer = shadowBuffer;
            GraphicsDevice.SetRenderTarget(0, shadowRT);
            GraphicsDevice.Clear(Color.White);
            forwardRenderer.DrawObjects(scene.TerrainVisuals, scene.ObjectVisuals);
            GraphicsDevice.DepthStencilBuffer = oldBuffer;
            GraphicsDevice.SetRenderTarget(0, oldTarget);
            shadowTexture = shadowRT.GetTexture();
            viewProjParameter.SetValue(oldViewProj);
        }

        private Matrix calculateLightViewProjection(SpotLight light)
        {
            float nearDistance = .001f;
            Vector3 orthogonal = Vector3.Cross(light.LookAt, Vector3.Forward);
            if (orthogonal == Vector3.Zero)
            {
                orthogonal = Vector3.Cross(light.LookAt, Vector3.Right);
            }
            Matrix view = Matrix.CreateLookAt(light.Position - (nearDistance * light.LookAt), light.Position + ((1 - nearDistance) * light.LookAt), orthogonal);
            float width = 2 * light.Radius * (float)Math.Tan(light.Angle / 2);
            Matrix perspective = Matrix.CreateOrthographic(width, width, nearDistance, light.Radius + nearDistance);

            return view * perspective;
        }

        private bool isCameraInCone(SpotLight light, CameraVisual camera)
        {
            Vector3 lightVector = Vector3.Normalize(light.Position - camera.Position);
            float SdL = Vector3.Dot(light.NormalLookAt, -lightVector);
            return SdL > light.AngleCosine;
        }
    }
}